package com.morder.net;

import static com.morder.util.Preconditions.checkArgument;
import static com.morder.util.Preconditions.checkNotNull;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

import android.content.Context;
import android.os.SystemClock;

import com.morder.base.Bytes;
import com.morder.base.ExceptionFlow;

/**
 * Issue HTTP post and get response.
 * <pre>
 * Post Usage:
 * new HttpExecutor.Post(context)
 *   .setUri("...")
 *   .addRequest("name", "value")
 *   .setResponseListener(new HttpExecutor.Listener() {
 *     public void onResponse(boolean success, byte[] response) {
 *       ...
 *     }
 *   })
 *   .execute()
 * 
 * Put Usage:
 * new HttpExecutor.Put(context)
 *   .setUri("...")
 *   .addRequest("name", "value")
 *   .setResponseListener(new HttpExecutor.Listener() {
 *     public void onResponse(boolean success, byte[] response) {
 *       ...
 *     }
 *   })
 *   .execute()
 * 
 * Get Usage:
 * new HttpExecutor.Get(context)
 *   .setUri("...")
 *   .setResponseListener(new HttpExecutor.Listener() {
 *     public void onResponse(boolean success, byte[] response) {
 *       ...
 *     }
 *   })
 *   .execute()
 * 
 * Delete Usage:
 * new HttpExecutor.Delete(context)
 *   .setUri("...")
 *   .setResponseListener(new HttpExecutor.Listener() {
 *     public void onResponse(boolean success, byte[] response) {
 *       ...
 *     }
 *   })
 *   .execute()
 *   </pre>
 */

public final class HttpExecutor {
  private HttpExecutor(){}

  public static interface Listener {
    public void OnResponse(boolean success, byte[] content);
  }

  static class Params {
    Context context = null;
    URI uri = null;
    HttpUriRequest request = null;
    ArrayList<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
    int tryTimes = 2;
    HttpExecutor.Listener listener = null;

    Params(final Context context) {
      this.context = context;
    }

    HttpExecutor apply(HttpExecutor h) {
      h.P = this;
      return h;
    }
  }

  Params P;

  public abstract static class RequestBase {
    Params p;

    public RequestBase(final Context context) {
      checkNotNull(context);
      p = new Params(context);
    }

    public RequestBase setUri(String uri) {
      checkArgument((uri != null) && (uri.length() > 0));
      try {
        p.uri = new URI(uri);
      } catch (URISyntaxException e) {
        throw new RuntimeException(e);
      }
      return this;
    }

    public RequestBase setResponseListener(HttpExecutor.Listener listener) {
      p.listener = listener;
      return this;
    }

    public RequestBase addRequest(String request, String content) {
      checkNotNull(request);
      checkNotNull(content);

      p.pairs.add(new BasicNameValuePair(request, content));
      return this;
    }

    public RequestBase setTryTimes(final int tryTimes) {
      checkArgument(tryTimes >= 1);

      p.tryTimes = tryTimes;
      return this;
    }

    public void execute() {
      p.request = createRequest();
      p.apply(new HttpExecutor()).execute();
    }

    protected abstract HttpUriRequest createRequest();
  }

  public static class Post extends RequestBase {
    public Post(final Context context) {
      super(context);
    }

    @Override
    public HttpUriRequest createRequest() {
      HttpPost post = new HttpPost(p.uri);
      post.setEntity(newEntity());
      return post;
    }

    HttpEntity newEntity() {
      HttpEntity entity = null;
      try {
        entity = new UrlEncodedFormEntity(p.pairs, HTTP.UTF_8);
      } catch (UnsupportedEncodingException e) {
        try {
          entity = new UrlEncodedFormEntity(p.pairs);
        } catch (UnsupportedEncodingException e1) {
          throw new RuntimeException(e1);
        }
      }
      return entity;
    }
  }

  public static class Put extends RequestBase {
    public Put(final Context context) {
      super(context);
    }

    @Override
    public HttpUriRequest createRequest() {
      HttpPut post = new HttpPut(p.uri);
      post.setEntity(newEntity());
      return post;
    }

    HttpEntity newEntity() {
      HttpEntity entity = null;
      try {
        entity = new UrlEncodedFormEntity(p.pairs, HTTP.UTF_8);
      } catch (UnsupportedEncodingException e) {
        try {
          entity = new UrlEncodedFormEntity(p.pairs);
        } catch (UnsupportedEncodingException e1) {
          throw new RuntimeException(e1);
        }
      }
      return entity;
    }
  }

  public static class Get extends RequestBase {
    public Get(final Context context) {
      super(context);
    }

    @Override
    public HttpUriRequest createRequest() {
      HttpGet get = new HttpGet(p.uri);
      return get;
    }
  }

  public static class Delete extends RequestBase {
    public Delete(final Context context) {
      super(context);
    }

    @Override
    public HttpUriRequest createRequest() {
      HttpDelete delete = new HttpDelete(p.uri);
      return delete;
    }
  }

  void execute() {
    byte[] response = Bytes.EMPTY_BYTE_ARRAY;

    boolean success = true;
    for(int i = 0; i < P.tryTimes; i++) {
      try {
        response = tryExecute();
      } catch (IOException e) {
        success = false;
      }

      if(success) {
        break;
      } else {
        SystemClock.sleep(2000);
      }
    }

    if(P.listener != null) {
      P.listener.OnResponse(success, response);
    }
  }

  byte[] tryExecute() throws IOException {
    HttpResponse response = requestResponse();
    int statusCode = response.getStatusLine().getStatusCode();
    if(statusCode != HttpStatus.SC_OK) {
      throw new IOException("Status code error:" + statusCode);
    }

    return parseResponse(response);
  }

  /**
   * Remember last choice.
   */
  static boolean applyApn = true;

  /**
   * Post with or without APN,
   * one case failed, try another.
   */
  HttpResponse requestResponse() throws IOException {
    //Apn apn = ApnProvider.getDefaultApn(P.context);
    //DefaultHttpClient client = newClient(apn, applyApn);
    DefaultHttpClient client = ClientFactory.newClient();
    HttpResponse response = null;
    try {
      response = exec(client);
    } catch (ApnException e) {
      if(applyApn) {
        ClientUtils.removeApn(client);
      }
      response = exec(client);
      applyApn = !applyApn;
    }
    
//    if(Apn.isUnValid(apn)) {
//      response = exec(client);
//    } else {
//      try {
//        response = exec(client);
//      } catch (ApnException e) {
//        if(applyApn) {
//          ClientUtils.removeApn(client);
//        } else {
//          ClientUtils.applyApn(client, apn);
//        }
//        response = exec(client);
//        applyApn = !applyApn;
//      }
//    }
    return response;
  }

  DefaultHttpClient newClient(final Apn apn, final boolean apply) {
    if(Apn.isUnValid(apn) || !apply) {
      return ClientFactory.newClient();
    } else {
      return ClientFactory.newClient(apn);
    }
  }

  /**
   * A SocketTimeoutException sometimes means APN problem.
   * Change the exception to ApnException.
   */
  HttpResponse exec(final DefaultHttpClient client) throws ApnException, IOException {
    checkNotNull(client);

    HttpResponse response = null;
    try {
      response = tryExec(client);
    } catch (SocketTimeoutException e) {
      throw new ApnException(e);
    } catch (ConnectTimeoutException e) {
      throw new ApnException(e);
    }
    return response;
  }

  /**
   * There is a bug in some version of DefaultHttpClient. A NullPointerException would be thrown
   * in some case.
   * To fix it, just ignore the exception and try execute again.
   */
  HttpResponse tryExec(final DefaultHttpClient client) throws IOException {
    HttpResponse response = null;
    for (int i = 0; i < 3; i++) {
      boolean tryAgain = false;
      try {
        response = client.execute(P.request);
      } catch (NullPointerException e) {
        ExceptionFlow.ignore(e);
        tryAgain = true;
      }
      if(!tryAgain) {
        break;
      } else {
        SystemClock.sleep(2000);
      }
    }
    return response;
  }

  /**
   * Exception caused by wrong APN settings.
   */
  static final class ApnException extends IOException {
    private static final long serialVersionUID = 1L;

    public ApnException(IOException e) {
      super(e.getMessage());
    }
  }

  byte[] parseResponse(final HttpResponse response) {
    checkNotNull(response);

    byte[] result = Bytes.EMPTY_BYTE_ARRAY;
    HttpEntity entity = response.getEntity();
    if(entity != null) {
      try {
        result = EntityUtils.toByteArray(entity);
      } catch (IOException e) {
        ExceptionFlow.ignore(e);
      } finally {
        try {
          entity.consumeContent();
        } catch (IOException e) {
          ExceptionFlow.ignore(e);
        }
      }
    }
    return result;
  }

}